iOS-Events[3]-Multitouch Events

本文对Multitouch Events的机制进行讲解。

iOS系统的Events类型分为以下几种:Multitouch events, Accelerometer events, Remote control events。

Events

Multitouch Events

在需要时,可以实现Touch方法,来控制Event的处理和传递,流程如下:

Creating a Subclass of UIResponder

要自定义Event的处理和传递,需要先继承这几个类:

  • UIView
  • UIViewController
  • UIControl
  • UIApplication/UIWindow

注意,接收Touches的View必须设置userInteractionEnabledYES,并且必须是可见的,不能是透明或者隐藏的。

Implementing the Touch-Event Handling Methods in Your Subclass

继承后,需要实现的UIResponder的方法为:

1
2
3
4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

注意,Cancellation是由于外部事件导致的,例如来电。

Tracking the Phase and Location of a Touch Event

Events

可以通过UITouch获取到phase, tapCount, location, previous location以及timestamp。

Retrieving and Querying Touch Objects

UIView的multipleTouchEnabled属性默认为NO,这意外着无论同时有多少个Touch发生,当前View只接收第一个,因此可以通过anyObject来获取当前Touch:

1
UITouch *touch = [touches anyObject];

如果需要获取不同对象上的Touch,可以通过以下方式:

1
2
3
[event allTouches];
[event touchesForWindow:self.window];
[event touchesForView:self];

以绘制用户的手写轨迹为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@property(nonatomic, assign) size_t handWriteID;
@property(nonatomic, strong) NSMutableArray *points;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
[_points addObject:[[Path alloc] initWithID:_handWriteID x:(int)point.x y:(int)point.y]];
[self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
[_points addObject:[[Path alloc] initWithID:_handWriteID x:(int)point.x y:(int)point.y]];
[self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
_handWriteID = 0;
[_points removeAllObjects];
[self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 5.0);
CGContextSetLineCap(context, kCGLineCapRound);
for(int i = 0; i < (int)(_points.count - 1); ++i){
Path *lastPoint = [_points objectAtIndex:i];
Path *nowPoint = [_points objectAtIndex:i + 1];
if(lastPoint.handWriteID == nowPoint.handWriteID){
CGContextMoveToPoint(context, lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(context, nowPoint.x, nowPoint.y);
CGContextStrokePath(context);
}
}
}

Handling One Touch Gestures

(1) Tap

1
2
3
4
5
6
7
8
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *aTouch in touches) {
if (aTouch.tapCount >= 2) {
// The view responds to the tap
[self respondToDoubleTapGesture:aTouch];
}
}
}

(2) Swipe

这里省略了中间点的判断,只简单判断开始和结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define HORIZ_SWIPE_DRAG_MIN  12
#define VERT_SWIPE_DRAG_MAX 4

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
// startTouchPosition is a property
self.startTouchPosition = [aTouch locationInView:self];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint currentTouchPosition = [aTouch locationInView:self];

// Check if direction of touch is horizontal and long enough
if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
{
// If touch appears to be a swipe
if (self.startTouchPosition.x < currentTouchPosition.x) {
[self myProcessRightSwipe:touches withEvent:event];
} else {
[self myProcessLeftSwipe:touches withEvent:event];
}
self.startTouchPosition = CGPointZero;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
self.startTouchPosition = CGPointZero;
}

(3) Drag

1
2
3
4
5
6
7
8
9
10
11
12
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint loc = [aTouch locationInView:self];
CGPoint prevloc = [aTouch previousLocationInView:self];

CGRect myFrame = self.frame;
float deltaX = loc.x - prevloc.x;
float deltaY = loc.y - prevloc.y;
myFrame.origin.x += deltaX;
myFrame.origin.y += deltaY;
[self setFrame:myFrame];
}

Handling Multitouch Gestures

对于同时处理多个Touch事件,需要先设置UIView的multipleTouchEnabled属性默认为YES,然后通过CFDictionaryRef来跟踪每一个Touch的phases(状态)。

注意:用CFDictionaryRef,而不是NSDictionary来跟踪,原因是NSDictionary会复制它的keys,而UITouch并没有适配NSCopying协议。

下面以对比起始点和结束点的位置为例,先记录起始点位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self cacheBeginPointForTouches:touches];
}

- (void)cacheBeginPointForTouches:(NSSet *)touches {
if ([touches count] > 0) {
for (UITouch *touch in touches) {
CGPoint *point = (CGPoint *)CFDictionaryGetValue(touchBeginPoints, touch);
if (point == NULL) {
point = (CGPoint *)malloc(sizeof(CGPoint));
CFDictionarySetValue(touchBeginPoints, touch, point);
}
*point = [touch locationInView:view.superview];
}
}
}

对比结束点位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
CGAffineTransform newTransform = [self incrementalTransformWithTouches:touches];
}

- (CGAffineTransform)incrementalTransformWithTouches:(NSSet *)touches {
NSArray *sortedTouches = [[touches allObjects] sortedArrayUsingSelector:@selector(compareAddress:)];

// Other code here
CGAffineTransform transform = CGAffineTransformIdentity;

UITouch *touch1 = [sortedTouches objectAtIndex:0];
UITouch *touch2 = [sortedTouches objectAtIndex:1];

CGPoint beginPoint1 = *(CGPoint *)CFDictionaryGetValue(touchBeginPoints, touch1);
CGPoint currentPoint1 = [touch1 locationInView:view.superview];
CGPoint beginPoint2 = *(CGPoint *)CFDictionaryGetValue(touchBeginPoints, touch2);
CGPoint currentPoint2 = [touch2 locationInView:view.superview];


// Compute the affine transform
return transform;
}

判断最后一根手势离开屏幕:

1
2
3
4
5
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
if ([touches count] == [[event touchesForView:self] count]) {
// Last finger has lifted
}
}

Specifying Custom Touch Event Behavior

自定义Touch Event行为,可以通过以下方式:

  • 支持多个MultiTouch的事件分发:设置View的multipleTouchEnabled为YES;
  • 限制单个View的事件分发:设置View的exclusiveTouch为NO,这意味着该View不能与其他View同时接受事件,如下图,如果先在B中触摸,再在A中触摸,则A不会收到事件,反之,则B不会收到事件,而B和C可以同时收到:

Events

  • 限制对SubViews的事件分发:重写hitTest:withEvents:方法,使得SubViews直接接收不到事件:
1
2
3
4
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return self;
}
  • 停止单个View的事件分发:设置View的userInteractionEnabled为NO;
  • 停止App在一段时间内的事件分发:
1
2
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
[[UIApplication sharedApplication] endIgnoringInteractionEvents];

Forwarding Touch Events

一般情况下,如果一个Response对象想要处理一个Touch,则该Touch的View对象必须指向该Response对象。如果想要转发Touch事件给其他View,则该Response对象必须是自定义的UIView子类。

转发Touch事件,有两种方式,一种是UIView通过在hit-testing方法中转发给SubViews,一种是重写UIWindow的sendEvents方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (void)sendEvent:(UIEvent *)event {
for (TransformGesture *gesture in transformGestures) {
// Collect all the touches you care about from the event
NSSet *touches = [gesture observedTouchesForEvent:event];
NSMutableSet *began = nil;
NSMutableSet *moved = nil;
NSMutableSet *ended = nil;
NSMutableSet *canceled = nil;

// Sort touches by phase to handle—-similar to normal event dispatch
for (UITouch *touch in touches) {
switch ([touch phase]) {
case UITouchPhaseBegan:
if (!began) began = [NSMutableSet set];
[began addObject:touch];
break;
case UITouchPhaseMoved:
if (!moved) moved = [NSMutableSet set];
[moved addObject:touch];
break;
case UITouchPhaseEnded:
if (!ended) ended = [NSMutableSet set];
[ended addObject:touch];
break;
case UITouchPhaseCancelled:
if (!canceled) canceled = [NSMutableSet set];
[canceled addObject:touch];
break;
default:
break;
}
}
// Call methods to handle the touches
if (began) [gesture touchesBegan:began withEvent:event];
if (moved) [gesture touchesMoved:moved withEvent:event];
if (ended) [gesture touchesEnded:ended withEvent:event];
if (canceled) [gesture touchesCancelled:canceled withEvent:event];
}
[super sendEvent:event];
}

Best Practices for Handling Multitouch Events

一些注意事项:

  • 实现Cancelation方法,保存状态;
  • 如果继承UIView, UIViewController和UIResponder:实现所有的Touches方法,不要调用super方法;
  • 如果是继承其他的UIKit类:不需要实现所有的Touches方法,一定要调用super方法;
  • 不要转发事件给其他的UIKit类对象,转发给UIView或子类对象,并且确保其可以接收转发的;
  • 不要显示地通过nextResponder方法发送事件到Responder Chain中;
  • 不要对Touch的坐标取整,因为这会导致精度丢失,在一般设备上,坐标系为320×480,而在高分辨率设备上,为640×960,这意味着可能边界存在着0.5个点的情况。